1 /* 2 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021 3 License: [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License]. 4 Authors: Marcelo S. N. Mancini 5 6 Copyright Marcelo S. N. Mancini 2018 - 2021. 7 Distributed under the CC BY-4.0 License. 8 (See accompanying file LICENSE.txt or copy at 9 https://creativecommons.org/licenses/by/4.0/ 10 */ 11 module hip.hiprenderer.shader.shadervar; 12 import hip.hiprenderer.shader.shader; 13 import hip.hiprenderer.renderer; 14 import hip.error.handler; 15 import hip.util.conv:to; 16 import hip.math.matrix; 17 import hip.api.graphics.color; 18 19 /** 20 * Changes how the Shader behaves based on the backend 21 */ 22 enum ShaderHint : uint 23 { 24 NONE = 0, 25 GL_USE_BLOCK = 1<<0, 26 GL_USE_STD_140 = 1<<1, 27 D3D_USE_HLSL_4 = 1<<2, 28 /** 29 * Meant for usage in uniform variables. 30 * That means one Shader Variable may not be sent to the backend depending on its requirements. 31 * An example for that is Array of Textures. In D3D11, it depends only on the resource being bound, 32 * while on Metal and GL3, they are required to be inside a MTLBuffer or being sent as an Uniform. 33 */ 34 Blackbox = 1 << 3, 35 MaxTextures = 1 << 4 36 } 37 38 /** 39 * Should not be used directly. The D type inference can already set that for you. 40 * This is stored by the variable to know how to access itself and comunicate the shader. 41 */ 42 enum UniformType 43 { 44 boolean, 45 integer, 46 integer_array, 47 uinteger, 48 uinteger_array, 49 floating, 50 floating2, 51 floating3, 52 floating4, 53 floating2x2, 54 floating3x3, 55 floating4x4, 56 floating_array, 57 ///Special type that is implemented by renderers backend 58 texture_array, 59 none 60 } 61 62 UniformType uniformTypeFrom(T)() 63 { 64 with(UniformType) 65 { 66 static if(is(T == bool)) return boolean; 67 else static if(is(T == int)) return integer; 68 else static if(is(T == int[])) return integer_array; 69 else static if(is(T == uint) || is(T == HipColor)) return uinteger; 70 else static if(is(T == uint[])) return uinteger_array; 71 else static if(is(T == float)) return floating; 72 else static if(is(T == float[2])) return floating2; 73 else static if(is(T == float[3])) return floating3; 74 else static if(is(T == float[4]) || is(T == HipColorf)) return floating4; 75 else static if(is(T == Matrix3)) return floating3x3; 76 else static if(is(T == Matrix4)) return floating4x4; 77 else static if(is(T == float[])) return floating_array; 78 else static if(is(T == IHipTexture[])) return texture_array; 79 else return none; 80 } 81 } 82 83 /** 84 * Struct that holds uniform/cbuffer information for Direct3D and OpenGL shaders. It can be any type. 85 * Its data is accessed by the ShaderVariableLayout when sendVars is called. Thus, depending on its 86 * corrensponding type, its data is uploaded to the GPU. 87 */ 88 struct ShaderVar 89 { 90 import hip.util.data_structures:Array; 91 import std.traits; 92 void[] data; 93 string name; 94 ShaderTypes shaderType; 95 UniformType type; 96 size_t singleSize; 97 bool isDynamicArrayReference; 98 99 bool isDirty = true; 100 private bool _isBlackboxed = false; 101 public bool isBlackboxed() const { return _isBlackboxed;} 102 103 104 size_t varSize() const{return data.length;} 105 size_t length() const {return varSize / singleSize;} 106 107 const T get(T)() 108 { 109 static if(isDynamicArray!T) 110 return cast(T)data; 111 else 112 return *(cast(T*)this.data.ptr); 113 } 114 115 bool setBlackboxed(T)(T value) 116 { 117 import core.stdc.string; 118 if(value.sizeof != varSize || !_isBlackboxed) return false; 119 isDirty = true; 120 memcpy(data.ptr, &value, varSize); 121 return true; 122 } 123 bool set(T)(T value, bool validateData) 124 { 125 import core.stdc.string; 126 static assert(uniformTypeFrom!T != UniformType.none, "Invalid type "~T.stringof); 127 static if(isDynamicArray!T) 128 { 129 memcpy(data.ptr, value.ptr, value.length * T.init[0].sizeof); 130 } 131 else 132 { 133 if(value.sizeof != varSize) 134 return false; 135 static if(is(T == Matrix3) || is(T == Matrix4)) 136 value = HipRenderer.getMatrix(value); 137 138 if(!_isBlackboxed && validateData && value == get!T) 139 return true; 140 memcpy(data.ptr, &value, varSize); 141 } 142 isDirty = true; 143 return true; 144 } 145 auto opAssign(T)(T value) 146 { 147 static if(is(T == ShaderVar)) 148 { 149 this.data = value.data; 150 this.name = value.name; 151 this.shaderType = value.shaderType; 152 this.singleSize = value.singleSize; 153 } 154 else 155 ErrorHandler.assertLazyExit(this.set(value), "Value set for '"~name~"' is invalid."); 156 return this; 157 } 158 159 private void throwOnOutOfBounds(size_t index) 160 { 161 switch(type) with(UniformType) 162 { 163 case integer_array: 164 ErrorHandler.assertExit(index < length/singleSize, "Index out of bounds on shader variable "~name); 165 break; 166 case uinteger_array: 167 ErrorHandler.assertExit(index < length/singleSize, "Index out of bounds on shader variable "~name); 168 break; 169 case floating_array: 170 ErrorHandler.assertExit(index < length/singleSize, "Index out of bounds on shader variable "~name); 171 break; 172 case floating2: 173 ErrorHandler.assertExit(index < 2, "Index out of bounds on shader variable "~name); 174 break; 175 case floating3: 176 ErrorHandler.assertExit(index < 3, "Index out of bounds on shader variable "~name); 177 break; 178 case floating4: 179 ErrorHandler.assertExit(index < 4, "Index out of bounds on shader variable "~name); 180 break; 181 case floating2x2: 182 ErrorHandler.assertExit(index < 4, "Index out of bounds on shader variable "~name); 183 break; 184 case floating3x3: 185 ErrorHandler.assertExit(index < 9, "Index out of bounds on shader variable "~name); 186 break; 187 case floating4x4: 188 ErrorHandler.assertExit(index < 16, "Index out of bounds on shader variable "~name); 189 break; 190 default: 191 ErrorHandler.assertExit(false, "opIndex is unsupported in var of type "~to!string(type)); 192 } 193 } 194 195 auto opIndexAssign(T)(T value, size_t index) 196 { 197 import core.stdc.string; 198 throwOnOutOfBounds(index); 199 ErrorHandler.assertExit(index*singleSize + T.sizeof <= varSize, "Value assign of type "~T.stringof~" at index "~to!string(index)~ 200 " is invalid for shader variable "~name~" of type "~to!string(type)); 201 memcpy(cast(ubyte*)data + singleSize*index, &value, T.sizeof); 202 return value; 203 } 204 205 ref auto opIndex(size_t index) 206 { 207 throwOnOutOfBounds(index); 208 switch(type) with(UniformType) 209 { 210 case integer_array: return get!(int[])[index]; 211 case uinteger_array: return get!(uint[])[index]; 212 case floating_array: return get!(float[])[index]; 213 case floating2: return get!(float[2])[index]; 214 case floating3: return get!(float[3])[index]; 215 case floating4: return get!(float[4])[index]; 216 case floating2x2: return get!(float[4])[index]; 217 case floating3x3: return get!(float[9])[index]; 218 case floating4x4: return get!(float[16])[index]; 219 default: 220 ErrorHandler.assertExit(false, "opIndex is unsupported in var of type "~to!string(type)); 221 return 0; 222 } 223 } 224 225 static ShaderVar* create(ShaderTypes t, string varName, bool data){return ShaderVar.create(t, varName, &data, UniformType.boolean, data.sizeof, data.sizeof);} 226 static ShaderVar* create(ShaderTypes t, string varName, int data){return ShaderVar.create(t, varName, &data, UniformType.integer, data.sizeof, data.sizeof);} 227 static ShaderVar* create(ShaderTypes t, string varName, uint data){return ShaderVar.create(t, varName, &data, UniformType.uinteger, data.sizeof, data.sizeof);} 228 static ShaderVar* create(ShaderTypes t, string varName, float data){return ShaderVar.create(t, varName, &data, UniformType.floating, data.sizeof, data.sizeof);} 229 static ShaderVar* create(ShaderTypes t, string varName, float[2] data){return ShaderVar.create(t, varName, &data, UniformType.floating2, data.sizeof, data[0].sizeof);} 230 static ShaderVar* create(ShaderTypes t, string varName, float[3] data){return ShaderVar.create(t, varName, &data, UniformType.floating3, data.sizeof, data[0].sizeof);} 231 static ShaderVar* create(ShaderTypes t, string varName, float[4] data){return ShaderVar.create(t, varName, &data, UniformType.floating4, data.sizeof, data[0].sizeof);} 232 static ShaderVar* create(ShaderTypes t, string varName, float[9] data){return ShaderVar.create(t, varName, &data, UniformType.floating3x3, data.sizeof, data[0].sizeof);} 233 static ShaderVar* create(ShaderTypes t, string varName, float[16] data){return ShaderVar.create(t, varName, &data, UniformType.floating4x4, data.sizeof, data[0].sizeof);} 234 static ShaderVar* create(ShaderTypes t, string varName, int[] data) 235 { 236 return ShaderVar.create(t, varName, data.ptr, UniformType.floating_array, int.sizeof*data.length, int.sizeof, true); 237 } 238 static ShaderVar* create(ShaderTypes t, string varName, uint[] data) 239 { 240 return ShaderVar.create(t, varName, data.ptr, UniformType.floating_array, uint.sizeof*data.length, uint.sizeof, true); 241 } 242 static ShaderVar* create(ShaderTypes t, string varName, float[] data) 243 { 244 return ShaderVar.create(t, varName, data.ptr, UniformType.floating_array, float.sizeof*data.length, float.sizeof, true); 245 } 246 247 protected static ShaderVar* create( 248 ShaderTypes t, 249 string varName, 250 void* varData, 251 UniformType type, 252 size_t varSize, 253 size_t singleSize, 254 bool isDynamicArrayReference=false 255 ) 256 { 257 import core.stdc.string : memcpy; 258 ErrorHandler.assertExit(isShaderVarNameValid(varName), "Variable '"~varName~"' is invalid."); 259 ShaderVar* s = new ShaderVar(); 260 s.data = new void[varSize]; 261 memcpy(s.data.ptr, varData, varSize); 262 s.name = varName; 263 s.shaderType = t; 264 s.type = type; 265 s.isDynamicArrayReference = isDynamicArrayReference; 266 s.singleSize = singleSize; 267 return s; 268 } 269 public static ShaderVar* createBlackboxed( 270 ShaderTypes t, 271 string varName, 272 UniformType type, 273 size_t varSize, 274 size_t singleSize) 275 { 276 ErrorHandler.assertExit(isShaderVarNameValid(varName), "Variable '"~varName~"' is invalid."); 277 ShaderVar* s = new ShaderVar(); 278 s.data = new void[varSize]; 279 s.name = varName; 280 s.singleSize = singleSize; 281 s.shaderType = t; 282 s.type = type; 283 return s; 284 } 285 286 void dispose() 287 { 288 type = UniformType.none; 289 shaderType = ShaderTypes.NONE; 290 singleSize = 0; 291 if(isDynamicArrayReference) 292 { 293 (cast(Array!(int)*)data).dispose(); 294 } 295 else if(data != null) 296 { 297 import core.memory; 298 GC.free(data.ptr); 299 data = null; 300 } 301 } 302 } 303 304 struct ShaderVarLayout 305 { 306 ShaderVar* sVar; 307 size_t alignment; 308 size_t size; 309 } 310 311 /** 312 * This class is meant to be created together with the Shaders. 313 * 314 * Those are meant to wrap the cbuffer from Direct3D and Uniform Block from OpenGL. 315 * 316 * By wrapping the uniforms/cbuffers layouts, it is much easier to send those variables from any API. 317 */ 318 class ShaderVariablesLayout 319 { 320 import hip.hiprenderer.shader.var_packing; 321 322 ShaderVarLayout[string] variables; 323 private string[] namesOrder; 324 private string[] unusedBlackboxed; 325 string name; 326 ShaderTypes shaderType; 327 protected Shader owner; 328 329 //Single block representation of variables content 330 protected void* data; 331 protected void* additionalData; 332 protected bool isAdditionalAllocated; 333 ///Can't unlock Layout 334 private bool isLocked; 335 336 ///The hint are used for the Shader backend as a notifier 337 public immutable int hint; 338 protected size_t lastPosition; 339 340 ///A function that must return a variable size when position = 0 341 private VarPosition function( 342 ref ShaderVar* v, 343 size_t lastAlignment, 344 bool isLast 345 ) packFunc; 346 347 348 /** 349 * Use the layout name for mentioning the uniform/cbuffer block name. 350 * 351 * Its members are the ShaderVar* passed 352 * 353 * Params: 354 * layoutName = From which block it will be accessed on the shader 355 * t = What is the shader type that holds those variables 356 * hint = Use ShaderHint for additional information, multiple hints may be passed 357 * variables = Usually you won't pass any and use .append for writing less 358 */ 359 this(string layoutName, ShaderTypes t, uint hint, ShaderVar*[] variables ...) 360 { 361 import core.stdc.stdlib:malloc; 362 this.name = layoutName; 363 this.shaderType = t; 364 this.hint = hint; 365 366 switch(HipRenderer.getRendererType()) 367 { 368 case HipRendererType.GL3: 369 // if(hint & ShaderHint.GL_USE_STD_140) 370 packFunc = &glSTD140; 371 break; 372 case HipRendererType.D3D11: 373 // if(hint & ShaderHint.D3D_USE_HLSL_4) 374 packFunc = &dxHLSL4; 375 break; 376 case HipRendererType.METAL: 377 packFunc = &glSTD140; 378 break; 379 case HipRendererType.NONE: 380 default:break; 381 } 382 if(packFunc is null) packFunc = &nonePack; 383 384 foreach(ShaderVar* v; variables) 385 { 386 ErrorHandler.assertExit(v.shaderType == t, "ShaderVariableLayout must contain only one shader type"); 387 ErrorHandler.assertExit((v.name in this.variables) is null, "Variable named "~v.name~" is already in the layout "~name); 388 this.variables[v.name] = ShaderVarLayout(v, 0, 0); 389 namesOrder~= v.name; 390 } 391 calcAlignment(); 392 data = malloc(getLayoutSize()); 393 ErrorHandler.assertExit(data != null, "Out of memory"); 394 } 395 396 static ShaderVariablesLayout from(T)() 397 { 398 enum attr = __traits(getAttributes, T); 399 static if(is(typeof(attr[0]) == HipShaderVertexUniform)) 400 enum shaderType = ShaderTypes.VERTEX; 401 else static if(is(typeof(attr[0]) == HipShaderFragmentUniform)) 402 enum shaderType = ShaderTypes.FRAGMENT; 403 else static assert(false, 404 "Type "~T.stringof~" doesn't have a HipShaderVertexUniform nor " ~ 405 "HipShaderFragmentUniform attached to it." 406 ); 407 static assert( 408 attr[0].name !is null, 409 "HipShaderUniform "~T.stringof~" must contain a name as it is required to work in Direct3D 11" 410 ); 411 ShaderVariablesLayout ret = new ShaderVariablesLayout(attr[0].name, shaderType, 0); 412 static foreach(mem; __traits(allMembers, T)) 413 {{ 414 alias member = __traits(getMember, T.init, mem); 415 alias a = __traits(getAttributes, member); 416 static if(is(typeof(a[0]) == ShaderHint) && a[0] & ShaderHint.Blackbox) 417 { 418 size_t length = 1; 419 if(a[0] & ShaderHint.MaxTextures) length = HipRenderer.getMaxSupportedShaderTextures(); 420 ret.appendBlackboxed(mem, uniformTypeFrom!(typeof(member)), length); 421 } 422 else 423 { 424 ret.append(mem, __traits(getMember, T.init, mem)); 425 } 426 427 }} 428 429 return ret; 430 } 431 432 Shader getShader(){return owner;} 433 void lock(Shader owner) 434 { 435 calcAlignment(); 436 this.owner = owner; 437 this.isLocked = true; 438 } 439 440 /** 441 * Calculates the shader variables alignment based on the packFunc passed at startup. 442 * Those functions are based on the shader vendor and version. Align should be called 443 * always when there is a change on the layout. 444 */ 445 final void calcAlignment() 446 { 447 size_t lastAlign = 0; 448 for(int i = 0; i < namesOrder.length; i++) 449 { 450 ShaderVarLayout* l = &variables[namesOrder[i]]; 451 VarPosition pos = packFunc(l.sVar, lastAlign, i == cast(int)namesOrder.length-1); 452 l.size = pos.size; 453 l.alignment = pos.startPos; 454 lastAlign = pos.endPos; 455 } 456 lastPosition = lastAlign; 457 } 458 459 460 void* getBlockData() 461 { 462 import core.stdc.string:memcpy; 463 foreach(v; variables) 464 memcpy(data+v.alignment, v.sVar.data.ptr, v.size); 465 return data; 466 } 467 468 protected ShaderVariablesLayout append(string varName, ShaderVar* v) 469 { 470 import core.stdc.stdlib:realloc; 471 ErrorHandler.assertExit((varName in variables) is null, "Variable named "~varName~" is already in the layout "~name); 472 ErrorHandler.assertExit(!isLocked, "Can't append ShaderVariable after it has been locked"); 473 variables[varName] = ShaderVarLayout(v, 0, 0); 474 namesOrder~= varName; 475 calcAlignment(); 476 this.data = realloc(this.data, getLayoutSize()); 477 ErrorHandler.assertExit(this.data != null, "Out of memory"); 478 return this; 479 } 480 481 /** 482 * Appends a new variable to this layout. 483 * Type is inferred. 484 */ 485 ShaderVariablesLayout append(T)(string varName, T data) 486 { 487 return append(varName, ShaderVar.create(this.shaderType, varName, data)); 488 } 489 /** 490 * Appends a new variable to this layout. 491 * Type is inferred. 492 */ 493 ShaderVariablesLayout appendBlackboxed(string varName, UniformType t, size_t length) 494 { 495 ShaderVar* sV = HipRenderer.createShaderVar(this.shaderType, t, varName, length); 496 if(sV is null) 497 { 498 unusedBlackboxed~= varName; 499 return this; 500 } 501 sV._isBlackboxed = true; 502 return append(varName, sV); 503 } 504 /** 505 * For speed sake, it doesn't check whether it is valid. 506 * That means both a valid and invalid variable would return false (meaning used.) 507 */ 508 bool isUnused(string varName) @nogc const 509 { 510 foreach(v; unusedBlackboxed) if(v == varName) return true; 511 return false; 512 } 513 514 final size_t getLayoutSize(){return lastPosition;} 515 final void setAdditionalData(void* d, bool isAllocated) 516 { 517 this.additionalData = d; 518 this.isAdditionalAllocated = isAllocated; 519 } 520 final const(void*) getAdditionalData() const {return cast(const(void*))additionalData;} 521 522 auto opDispatch(string member)() 523 { 524 return variables[member].sVar; 525 } 526 527 void dispose() 528 { 529 import core.stdc.stdlib:free; 530 foreach (ref v; variables) 531 { 532 v.sVar.dispose(); 533 v.alignment = 0; 534 v.size = 0; 535 v.sVar = null; 536 } 537 if(data != null) 538 free(data); 539 if(isAdditionalAllocated && additionalData != null) 540 free(additionalData); 541 additionalData = null; 542 data = null; 543 } 544 } 545 546 547 private bool isShaderVarNameValid(ref string varName) 548 { 549 import hip.util.string : indexOf; 550 551 return varName.length > 0 && 552 varName.indexOf(" ") == -1; 553 }